// this is a WSH compatibility stub
// at least two advantages over a batch "start node xxx.js" file:
// 1, double click this file will work (windows only of cause
// 2, no additional file
if((function detect_rt(){
	try{
		if("process" in global){
			return "node";
		}
	}catch(_){
		if(WSH != undefined){
			return "WSH";
		}
	}
})() == "WSH"){
	// WSH.Echo(WSH.ScriptFullName);
	WSH.CreateObject("Shell.Application").ShellExecute("node", WSH.ScriptFullName, "", "", 1);
	WSH.Quit();
}

// you can write the rest of the file safely assuming they will be running in node
// it still needs to be lexically compatible with JScript, like no trailing comma after the last entry in an object literal

var fs = require('fs');
var http = require('http');
var util = require('util');

var UTF8 = "utf8";
var FSOPTS = {encoding: UTF8};

var ALL_IF = "0.0.0.0";
var LO = "127.0.0.1";
var PAC_DIR = "pac/";
var CONF = "conf/servers.conf";
var CONF_FILTER = "conf/blacklist.conf";

function read_servers(filename){
	var servers_string = fs.readFileSync(filename, FSOPTS);
	var server_re = /^([^=]+)=([^=:]+):([0-9,]+)\/([0-9a-fA-F]+)$/mg;
	var res, servers = [];
	while(res = server_re.exec(servers_string)){
		var hosts = res[2].split(",");
		var ports = res[3].split(",");
		hosts.forEach(function(host, i){
			ports.forEach(function(port){
				servers.push({
					name: (hosts.length > 1 ? res[1] + i : res[1]),
					host: host, port: port, key: res[4]
				});
			});
		});
	}
	return servers;
}

function get_address(family, external){
	var ifaces = require('os').networkInterfaces();
	var ret = [];
	for (var iface in ifaces){
		var addrs = ifaces[iface];
		for (var i in addrs){
			var addr = addrs[i];
			if ((addr.internal ^ external) && (family == undefined || addr.family == family)){
				ret.push(addr.address);
			}
		}
	}
	return ret;
}

function format_string(pattern, kwargs){
	// like % in python
	for (var k in kwargs){
		var v = kwargs[k];
		var r = new RegExp('%\\(' + k + '\\)s', 'g');
		pattern = pattern.replace(r, v);
	}
	return pattern;
}

function gen_pac(socks_ip, socks_port, filter){
	var ta = fs.readFileSync('template/pac.js', FSOPTS);
	var t = fs.readFileSync('template/pac_static.js', FSOPTS);
	return [["SOCKS5", ta], ["SOCKS", ta], ["SOCKS", t], ["PROXY", ta]].map(function(e){
		var p = e[0] + " " + socks_ip + ":" + socks_port + "; DIRECT";
		return new Buffer(format_string(e[1], {proxy: p, pattern: filter}), UTF8);
	});
}

function compile_filter(list_str){
	var hosts = [];
	list_str.split('\n').forEach(function(line){
		var host = line.trim();
		if (host.length != 0 && host.slice(0, 1) != '#'){
			hosts.push(host.split('.').join('\\.'));
		}
	});
	return '^(.+\\.)?(' + (hosts.join('|')) + ')$';
}

function make_filter_cb(filter){
	var filter = new RegExp(filter);
	return function(h){
		var ret = filter.test(h);
		util.log("\t" + h + ", " + ret);
		return ret;
	}
}

var CT_PAC = "application/x-ns-proxy-autoconfig";
var CT_JSON = "application/json";

function pico_httpd(files){
	var e404 = new Buffer('not found', 'utf-8');
	var s = http.createServer(function(req, res){
		util.log('HTTPD: ' + req.method + ' ' + req.url + ' from: ' + req.connection.remoteAddress + ':' + req.connection.remotePort);
		var data = files[req.url.slice(1)]; // omit the beginning slash
		if(data != undefined){
			res.writeHead(200, {
				"Content-Length": data[1].length,
				"Content-Type": data[0]
			});
			res.end(data[1]);
		}else{
			res.writeHead(404, {
				"Content-Type": "text/plain"
			});
			res.end(e404);
		}

	});
	s.on('listening', function(){
		var addr = s.address();
		var file_names = [];
		for(var n in files){
			file_names.push(n);
		}
		file_names.sort();
		util.log('HTTPD: listening on ' + addr.address + ':' + addr.port + ", files:\n" + (file_names.join("\n")));
	});
	return s;
}

function gen_switchy(servers, ext_ip, socks_port, pac_port){
	var bypass_list = [];
	var reserved_addresses = ["<local>", "127.0.0.0/8", "192.168.0.0/16", "10.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12"];
	for (var i = 0; i < reserved_addresses.length; ++i){
		bypass_list.push({
			"conditionType": "BypassCondition",
			"pattern": reserved_addresses[i]
		});
	}
	var switchy = {
		"schemaVersion": 2,
		"-confirmDeletion": true,
		"-downloadInterval": 1440,
		"-enableQuickSwitch": false,
		"-quickSwitchProfiles": [],
		"-refreshOnProfileChange": false,
		"-showInspectMenu": false,
		"-startupProfileName": ""
	};
	for (var i = 0; i < servers.length; ++i){
		var server = servers[i];
		var name = server.port == 443 ? server.name + ":443" : server.name
		switchy["+AUTO " + name] = {
			"profileType": "PacProfile",
			"name": "AUTO " + name,
			"color": "#d497ee",
			// revision = 0 is OK according to https://github.com/FelisCatus/SwitchyOmega/issues/260
			"revision": "0",
			"pacUrl": "http://" + ext_ip + (pac_port == 80? "" : ":" + pac_port) + "/" + PAC_DIR
				+ server.name + (server.port == 443 ? "-443-a5.pac" : "-a5.pac")
		};
		switchy["+SOCKS5 " + name] = {
			"profileType": "FixedProfile",
			"name": "SOCKS5 " + name,
			"color": "#55bb55",
			"revision": "0",
			"fallbackProxy": {
				"scheme": "socks5",
				"port": socks_port + i,
				"host": ext_ip
			},
			"bypassList": bypass_list
		};
	}
	return switchy;
}

function multi_ass(servers, socks_port, cond_port, cb_filter){
	var ass = require('./ass.js');
	var daemons = [];
	servers.forEach(function(s, i){
		var key = new Buffer(s.key, "hex");
		var d = ass.create_client('rc4', key, s.host, s.port, null);
		d.listen(socks_port + i, ALL_IF);
		daemons.push(d);
		d = ass.create_client('rc4', key, s.host, s.port, cb_filter);
		d.listen(cond_port + i, ALL_IF);
		daemons.push(d);
	});
	register_status_callback(function(){
		daemons.forEach(function(d){
			d.getConnections(function(err, count){
				util.log(count + " local connections on " + d.address().port);
			});
		});
	});
}

function generate_files(servers, ext_ip, socks_port, pac_port, filter){
	var files = {};
	servers.forEach(function(s, i){
		var pacs = gen_pac(ext_ip, socks_port + i, filter);
		var name = s.port == 443 ? s.name + "-443" : s.name;
		["a5", "a", "s", "h"].forEach(function(n, j){
			files[PAC_DIR + name + "-" + n + ".pac"] = [CT_PAC, pacs[j]];
		});
		if(i == 0){
			// special alias for the first entry
			["auto5", "auto", "socks", "http"].forEach(function(n, j){
				files[PAC_DIR + n + ".pac"] = [CT_PAC, pacs[j]];
			});
		}
	});
	files["conf/switchy.json"] = [CT_JSON, JSON.stringify(gen_switchy(servers, ext_ip, socks_port, pac_port))];
	return files;
}

function bench(socks_port, servers, repeat_count){
	var TEST_SOURCE = "www.google.com";
	var nul_device = process.platform == "win32" ? "nul" : "/dev/null";
	function rec_test(name, proxy, remain_count, success_count, total_time){
		var start = (new Date).getTime();
		child_process.exec("curl -o " + nul_device
			+ " --socks5-hostname " + proxy
			+ " " + TEST_SOURCE,
			function(err, stdout, stderr){
			if(!err){
				success_count += 1;
			}
			total_time += (new Date).getTime() - start;
			if(remain_count > 1){
				rec_test(name, proxy,
					remain_count - 1,
					success_count,
					total_time);
			}else{
				console.log(name
					+ " success/fail: " + success_count
					+ "/" + (repeat_count - success_count)
					+ ", cost " + (total_time / 1000) + "s");
			}
		});
	}
	servers.forEach(function(server, i){
		rec_test(server.port == 443 ? server.name + ":443" : server.name,
			"127.0.0.1:" + (socks_port + i), repeat_count, 0, 0);
	});
}

function update_file(path, content){
	if(fs.existsSync(path) && fs.readFileSync(path, FSOPTS) == content){
		util.log(path + ' is up to date')
	}else{
		fs.writeFileSync(path, content, FSOPTS);
		util.log(path + ' updated')
	}
}

function get_ext_ip(){
	var ext_ip = get_address('IPv4', true);
	if(ext_ip.length == 1){
		ext_ip = ext_ip[0];
		util.log('assuming external IP: ' + ext_ip);
		return ext_ip;
	}else{
		util.log('multiple external IPs detected: ' + (ext_ip.join(", "))
				+ ", fallback to " + LO);
		return LO;
	}
}

function register_status_callback(cb){
	process.on(process.platform == "win32" ? "SIGBREAK" : "SIGUSR2", function(){
		util.log("uptime: " + process.uptime());
		var mem = process.memoryUsage();
		util.log("rss: " + mem.rss + ", V8 heapTotal: " + mem.heapTotal + ", heapUsed: " + mem.heapUsed);
		cb();
	});
}

if(!module.parent){
	process.title = "helvetica";
	// get command line arguments
	var argv = process.argv;
	if(argv.length == 2 && !("jxcore" in global)){
		// the new standard mode
		// simply put, this is multi + gen + PACd
		var socks_port = 8000;
		var conditional_port = 8200;
		var pac_port = 8080;
		var ext_ip = get_ext_ip();
		var servers = read_servers(CONF);
		var filter = compile_filter(fs.readFileSync(CONF_FILTER, FSOPTS));
		var cb_filter = make_filter_cb(filter);
		multi_ass(servers, socks_port, conditional_port, cb_filter);
		var files = generate_files(servers, ext_ip, socks_port, pac_port, filter);
		var httpd = pico_httpd(files);
		httpd.listen(pac_port, ALL_IF);
	}else if("jxcore" in global || (argv.length == 3 && argv[2] == "android")){
		// for JXcore on Android
		var readline = require("readline");
		var socks_port = 1080;
		var servers = read_servers(CONF);
		var CODE_A = "a".charCodeAt(0);
		servers.forEach(function(s, i){
			console.log(String.fromCharCode(CODE_A + i) + " " + s.name + ", " + s.host + ":" + s.port);
		});
		var rl = readline.createInterface({input: process.stdin, output: process.stdout});
		var question = "enter your choice: ";
		function answer(c){
			var i = null;
			if(c.length == 0){
				i = 0;
			}else if(c.length == 1){
				i = c.charCodeAt(0) - CODE_A;
			}
			if(i != null && i >= 0 && i <= servers.length){
				rl.close();
				var s = servers[i];
				var ass = require("./ass.js");
				var key = new Buffer(s.key, "hex");
				var d = ass.create_client("rc4", key, s.host, s.port, null);
				d.listen(socks_port, LO);
				var cb_filter = make_filter_cb(compile_filter(fs.readFileSync(CONF_FILTER, FSOPTS)));
				var c = ass.create_client("rc4", key, s.host, s.port, cb_filter);
				c.listen(socks_port + 1, LO);
			}else{
				console.log("invalid input: " + c);
				rl.question(question, answer);
			}
		}
		rl.question(question, answer);
	}else if(argv.length == 5 || argv.length == 6){
		// old standard mode, deprecated
		var ass = require('./ass.js');

		var key = new Buffer(argv[2], 'hex');
		var server_ip = argv[3];
		var server_port = parseInt(argv[4]);

		var socks_port = 1080;
		var conditional_port = 1081;
		var pac_port = 8080;
		var ext_ip = argv.length == 6 ? argv[5] : get_ext_ip();

		var ass0 = ass.create_client('rc4', key, server_ip, server_port, null);
		ass0.listen(socks_port, ALL_IF);

		var filter = compile_filter(fs.readFileSync(CONF_FILTER, FSOPTS))
		var cb_filter = make_filter_cb(filter);
		var ass1 = ass.create_client('rc4', key, server_ip, server_port, cb_filter);
		ass1.listen(conditional_port, ALL_IF);

		var pacs = gen_pac(ext_ip, socks_port, filter);
		var files = {};
		["auto/socks5", "auto/socks", "socks5", "socks"].forEach(function(n, j){
			files[n + ".pac"] = [CT_PAC, pacs[j]];
		});
		var pacd = pico_httpd(files);
		pacd.listen(pac_port, ALL_IF);
	}else if((argv.length == 3 || argv.length == 4) && argv[2] == 'multi'){
		// multiple tunnels mode, no pac
		// reads servers.conf
		var socks_port = 8000;
		var cond_port = 8200;
		var servers = read_servers(argv.length == 3 ? CONF : argv[3]);
		var cb_filter = make_filter_cb(compile_filter(fs.readFileSync(CONF_FILTER, FSOPTS)));
		multi_ass(servers, socks_port, cond_port, cb_filter);
	}else if((argv.length == 3 || argv.length == 4) && argv[2] == 'gen'){
		// generate pac, and switchy conf
		// reads servers.conf
		var socks_port = 8000;
		var conditional_port = 8200;
		var pac_port = 8080;
		var ext_ip = argv.length == 4 ? argv[3] : get_ext_ip();

		var servers = read_servers(CONF);
		var filter = compile_filter(fs.readFileSync(CONF_FILTER, FSOPTS));
		var files = generate_files(servers, ext_ip, socks_port, pac_port, filter);
		for (var n in files){
			update_file(n, files[n][1]);
		}
	}else if((argv.length == 3 || argv.length == 4) && argv[2] == "bench"){
		var servers = read_servers(CONF);
		bench(8000, servers, argv.length == 3 ? 10 : parseInt(argv[3]));
	}else{
		// TODO: better arguments parsing
		console.error('invalid arguments');
	}
}

